|
1
|
|
|
import qs from 'qs'; |
|
2
|
|
|
import { isEmpty, isNil, reject } from 'ramda'; |
|
3
|
|
|
import { AxiosInstance, AxiosResponse, Method } from 'axios'; |
|
4
|
|
|
import { ShortUrlsListParams } from '../../short-urls/reducers/shortUrlsListParams'; |
|
5
|
|
|
import { ShortUrl, ShortUrlData } from '../../short-urls/data'; |
|
6
|
|
|
import { OptionalString } from '../utils'; |
|
7
|
|
|
import { |
|
8
|
|
|
ShlinkHealth, |
|
9
|
|
|
ShlinkMercureInfo, |
|
10
|
|
|
ShlinkShortUrlsResponse, |
|
11
|
|
|
ShlinkTags, |
|
12
|
|
|
ShlinkTagsResponse, |
|
13
|
|
|
ShlinkVisits, |
|
14
|
|
|
ShlinkVisitsParams, |
|
15
|
|
|
ShlinkShortUrlMeta, |
|
16
|
|
|
ShlinkDomain, |
|
17
|
|
|
ShlinkDomainsResponse, |
|
18
|
|
|
ShlinkVisitsOverview, |
|
19
|
|
|
} from './types'; |
|
20
|
|
|
|
|
21
|
24 |
|
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : ''; |
|
22
|
2 |
|
const rejectNilProps = reject(isNil); |
|
23
|
|
|
|
|
24
|
|
|
export default class ShlinkApiClient { |
|
25
|
|
|
private apiVersion: number; |
|
26
|
|
|
|
|
27
|
|
|
public constructor( |
|
28
|
|
|
private readonly axios: AxiosInstance, |
|
29
|
|
|
private readonly baseUrl: string, |
|
30
|
|
|
private readonly apiKey: string, |
|
31
|
|
|
) { |
|
32
|
28 |
|
this.apiVersion = 2; |
|
33
|
|
|
} |
|
34
|
|
|
|
|
35
|
28 |
|
public readonly listShortUrls = async (params: ShortUrlsListParams = {}): Promise<ShlinkShortUrlsResponse> => |
|
36
|
1 |
|
this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', params) |
|
37
|
1 |
|
.then(({ data }) => data.shortUrls); |
|
38
|
|
|
|
|
39
|
28 |
|
public readonly createShortUrl = async (options: ShortUrlData): Promise<ShortUrl> => { |
|
40
|
4 |
|
const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options as any); |
|
41
|
|
|
|
|
42
|
2 |
|
return this.performRequest<ShortUrl>('/short-urls', 'POST', {}, filteredOptions) |
|
43
|
2 |
|
.then((resp) => resp.data); |
|
44
|
|
|
}; |
|
45
|
|
|
|
|
46
|
28 |
|
public readonly getShortUrlVisits = async (shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits> => |
|
47
|
1 |
|
this.performRequest<{ visits: ShlinkVisits }>(`/short-urls/${shortCode}/visits`, 'GET', query) |
|
48
|
1 |
|
.then(({ data }) => data.visits); |
|
49
|
|
|
|
|
50
|
28 |
|
public readonly getTagVisits = async (tag: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> => |
|
51
|
1 |
|
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query) |
|
52
|
1 |
|
.then(({ data }) => data.visits); |
|
53
|
|
|
|
|
54
|
28 |
|
public readonly getVisitsOverview = async (): Promise<ShlinkVisitsOverview> => |
|
55
|
1 |
|
this.performRequest<{ visits: ShlinkVisitsOverview }>('/visits', 'GET') |
|
56
|
1 |
|
.then(({ data }) => data.visits); |
|
57
|
|
|
|
|
58
|
28 |
|
public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise<ShortUrl> => |
|
59
|
3 |
|
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain }) |
|
60
|
3 |
|
.then(({ data }) => data); |
|
61
|
|
|
|
|
62
|
28 |
|
public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> => |
|
63
|
3 |
|
this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain }) |
|
64
|
|
|
.then(() => {}); |
|
65
|
|
|
|
|
66
|
28 |
|
public readonly updateShortUrlTags = async ( |
|
67
|
|
|
shortCode: string, |
|
68
|
|
|
domain: OptionalString, |
|
69
|
|
|
tags: string[], |
|
70
|
|
|
): Promise<string[]> => |
|
71
|
3 |
|
this.performRequest<{ tags: string[] }>(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags }) |
|
72
|
3 |
|
.then(({ data }) => data.tags); |
|
73
|
|
|
|
|
74
|
28 |
|
public readonly updateShortUrlMeta = async ( |
|
75
|
|
|
shortCode: string, |
|
76
|
|
|
domain: OptionalString, |
|
77
|
|
|
meta: ShlinkShortUrlMeta, |
|
78
|
|
|
): Promise<ShlinkShortUrlMeta> => |
|
79
|
3 |
|
this.performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta) |
|
80
|
3 |
|
.then(() => meta); |
|
81
|
|
|
|
|
82
|
28 |
|
public readonly listTags = async (): Promise<ShlinkTags> => |
|
83
|
1 |
|
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' }) |
|
84
|
1 |
|
.then((resp) => resp.data.tags) |
|
85
|
1 |
|
.then(({ data, stats }) => ({ tags: data, stats })); |
|
86
|
|
|
|
|
87
|
28 |
|
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> => |
|
88
|
1 |
|
this.performRequest('/tags', 'DELETE', { tags }) |
|
89
|
1 |
|
.then(() => ({ tags })); |
|
90
|
|
|
|
|
91
|
28 |
|
public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> => |
|
92
|
1 |
|
this.performRequest('/tags', 'PUT', {}, { oldName, newName }) |
|
93
|
1 |
|
.then(() => ({ oldName, newName })); |
|
94
|
|
|
|
|
95
|
28 |
|
public readonly health = async (): Promise<ShlinkHealth> => |
|
96
|
1 |
|
this.performRequest<ShlinkHealth>('/health', 'GET') |
|
97
|
1 |
|
.then((resp) => resp.data); |
|
98
|
|
|
|
|
99
|
28 |
|
public readonly mercureInfo = async (): Promise<ShlinkMercureInfo> => |
|
100
|
1 |
|
this.performRequest<ShlinkMercureInfo>('/mercure-info', 'GET') |
|
101
|
1 |
|
.then((resp) => resp.data); |
|
102
|
|
|
|
|
103
|
28 |
|
public readonly listDomains = async (): Promise<ShlinkDomain[]> => |
|
104
|
1 |
|
this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains.data); |
|
105
|
|
|
|
|
106
|
28 |
|
private readonly performRequest = async <T>(url: string, method: Method = 'GET', query = {}, body = {}): Promise<AxiosResponse<T>> => { |
|
107
|
24 |
|
try { |
|
108
|
24 |
|
return await this.axios({ |
|
109
|
|
|
method, |
|
110
|
|
|
url: `${buildShlinkBaseUrl(this.baseUrl, this.apiVersion)}${url}`, |
|
111
|
|
|
headers: { 'X-Api-Key': this.apiKey }, |
|
112
|
|
|
params: rejectNilProps(query), |
|
113
|
|
|
data: body, |
|
114
|
|
|
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }), |
|
115
|
|
|
}); |
|
116
|
|
|
} catch (e) { |
|
117
|
|
|
const { response } = e; |
|
118
|
|
|
|
|
119
|
|
|
// Due to a bug on all previous Shlink versions, requests to non-matching URLs will always result on a CORS error |
|
120
|
|
|
// when performed from the browser (due to the preflight request not returning a 2xx status. |
|
121
|
|
|
// See https://github.com/shlinkio/shlink/issues/614), which will make the "response" prop not to be set here. |
|
122
|
|
|
// The bug will be fixed on upcoming Shlink patches, but for other versions, we can consider this situation as |
|
123
|
|
|
// if a request has been performed to a not supported API version. |
|
124
|
|
|
const apiVersionIsNotSupported = !response; |
|
125
|
|
|
|
|
126
|
|
|
// When the request is not invalid or we have already tried both API versions, throw the error and let the |
|
127
|
|
|
// caller handle it |
|
128
|
4 |
|
if (!apiVersionIsNotSupported || this.apiVersion === 1) { |
|
129
|
|
|
throw e; |
|
130
|
|
|
} |
|
131
|
|
|
|
|
132
|
|
|
this.apiVersion = this.apiVersion - 1; |
|
133
|
|
|
|
|
134
|
|
|
return await this.performRequest(url, method, query, body); |
|
135
|
|
|
} |
|
136
|
|
|
}; |
|
137
|
|
|
} |
|
138
|
|
|
|